from dataclasses import dataclass
from dotenv import load_dotenv
import json
import os
import sys
import aiohttp
import asyncio
import requests
from typing import Mapping, cast, Tuple, List
load_dotenv()
API_KEY = os.getenv('API_KEY')
if not API_KEY:
    raise ValueError("API_KEY is not set")
headers: Mapping[str, str] = {
    'Accept': 'application/json',
    'x-api-Key': API_KEY
}
API_URL = "https://api.curseforge.com/"


@dataclass(frozen=True)
class AManifestFile:
    projectID: int
    fileID: int
    required: bool


@dataclass(frozen=True)
class ALatestFile:
    id: int
    modId: int
    fileName: str


@dataclass
class DownloadTask:
    url: str


def compare_files(old_files: list[AManifestFile], new_files: list[AManifestFile]):
    old_dicts = {file.projectID: file for file in old_files}
    new_dicts = {file.projectID: file for file in new_files}
    old_ids = set(old_dicts.keys())
    new_ids = set(new_dicts.keys())
    added = [new_dicts[pid] for pid in (new_ids - old_ids)]
    removed = [old_dicts[pid] for pid in (old_ids - new_ids)]
    updated: List[AManifestFile] = []
    common_ids = old_ids & new_ids
    for i in common_ids:
        old_file = old_dicts[i]
        new_file = new_dicts[i]
        if old_file.fileID != new_file.fileID:
            updated.append(new_file)
    return added, removed, updated


async def fetch_download_url(session: aiohttp.ClientSession, file: AManifestFile, semaphores: asyncio.Semaphore):
    async with semaphores:
        url = f"{API_URL}v1/mods/{file.projectID}/files/{file.fileID}/download-url"
        try:
            async with session.get(url, headers=headers) as r:
                r.raise_for_status()
                data = await r.json()
                return DownloadTask(data['data'])
        except Exception as e:
            return file


async def get_mod_info_url(session: aiohttp.ClientSession, file: AManifestFile, semaphores: asyncio.Semaphore):
    async with semaphores:
        url = f"{API_URL}v1/mods/{file.projectID}"
        try:
            async with session.get(url, headers=headers) as r:
                r.raise_for_status()
                data = await r.json()
                mod_url = data['data']['links']['websiteUrl']
                if isinstance(mod_url, str):
                    return f"{mod_url}/download/{file.fileID}"
                else:
                    return f"Can not get {file.projectID}/{file.fileID} download url\n"
        except Exception as e:
            return f"Can not get {file.projectID}/{file.fileID} download url\n"


async def get_mod_name(session: aiohttp.ClientSession, file: AManifestFile, semaphores: asyncio.Semaphore):
    async with semaphores:
        url = f"{API_URL}v1/mods/{file.projectID}"
        try:
            async with session.get(url, headers=headers) as r:
                r.raise_for_status()
                data = await r.json()
                name = data['data']['slug']
                if isinstance(name, str):
                    return name
                else:
                    raise RuntimeError(
                        f"Can not get {file.projectID}/{file.fileID} name")
        except Exception as e:
            print(e)
            raise e


async def main():
    with open('./manifest.json', 'r', encoding='utf-8') as f:
        data = json.loads(f.read())
        files = [AManifestFile(**file) for file in data['files']]
        nums = len(files)
        print(f"{nums} mods")
        semaphores = asyncio.Semaphore(5)
        async with aiohttp.ClientSession() as session:
            files_to_down = files
            if os.path.exists('old.json'):
                with open('./old.json', 'r', encoding='utf-8') as old:
                    old_data = json.loads(old.read())
                    old_files = [AManifestFile(**file)
                                 for file in old_data['files']]
                    diff = compare_files(old_files, files)
                    for removed in diff[1]:
                        filename = await get_mod_name(session, removed, semaphores)
                        print(f"{filename} removed")
                    for added in diff[0]:
                        filename = await get_mod_name(session, added, semaphores)
                        print(f"{filename} added")
                    for updated in diff[2]:
                        filename = await get_mod_name(session, updated, semaphores)
                        print(f"{filename} updated")
                    files_to_down = diff[0] + diff[2]
            else:
                print("old.json not found")
            tasks = [fetch_download_url(session, file, semaphores)
                     for file in files_to_down]
            with open('./download.txt', 'w', encoding='utf-8') as url_files:
                count = 0
                for future in asyncio.as_completed(tasks):
                    result = await future
                    if isinstance(result, AManifestFile):
                        msg = await get_mod_info_url(session, result, semaphores)
                        print(msg)
                        count += 1
                    if isinstance(result, DownloadTask):
                        url_files.write(f"{result.url}\n")

    print(f"{count}/{nums} mods download failed")


asyncio.run(main())
